/**
 * vue是mvvm框架，mvvm双向数据绑定（数据更新，视图更新，视图更新，数据更新），单项数据流（数据从上往下流）
 * 虚拟dom  就是用普通的js对象去模拟了真实dom
 * 指令 凡是v-开头的行内属性，我们都称为vue指令
 * v-model主要应用于表单元素上，也可以用在子组件上
 * v-text 主要是为了解决小胡子的显示问题，90%用不上
 * v-html 等同于innerHTML
 * v-clock 需要配合css使用
 * v-pre 告诉vue这个代码块不用解析了，直接拿过去
 * v-if 控制整个结构的加载还是不加载，有比较大的切换开销，有比较小的初始加载开销
 * v-show 单纯的控制display，有比较小的切换开销，有比较大的初始加载开销
 * v-for 
 * key  为虚拟dom服务的，相当于这个元素的唯一标识，类似于身份证。最理想的key是这条数据的唯一id；index；key可以提升虚拟dom的编译效率
 * 当v-if和v-for同时使用的时候，v-for的优先级高（2.0），在3.0中v-if优先级高
 * v-bind   专门处理行内属性
 * proxy叫代理，defineProperty劫持
 * data中的属性和methods中的属性是不能重复的，methods中属性不能是箭头函数，箭头函数中的this指向window
 * @click="fn(item, $event)" $event传事件对象，写死的
 * .stop 阻止冒泡
 * .prevent 阻止默认事件
 * .capture 在捕获阶段执行，默认在冒泡阶段执行
 * .self 只有点击当前元素的时候才会执行；冒泡传过来的不会执行
 * .once 只执行一次
 * .passive 优先执行默认行为还是绑定的事件
 * @keydow='fn
 * lazy修饰符，input事件内容发生改变就会触发，change事件内容发生改变并且失去光标就会触发'
 * .sycn
 * 数组劫持的方法 shift unshift pop push sort splice reserve
 * 
 * diff算法
 * 比较新老节点，只会同级比较
 * 新节点的文本节点是否存在，如果不存在
 * 
 *  如果老节点有子节点，新节点没有，新节点直接替换掉老节点
 *  老节点中没有子节点，新节点中有子节点，将新节点中子节点加入到老节点中
 *  新老节点均有子节点，对子节点进行diff
 *      从源码上看，定义了四个索引，分别是老的开始索引， 老的结束索引，新的开始索引，新的结束索引
 *      从头开始和从尾部同时开始比较，往中间比较
 *      老的开始节点和新的开始节点一样，递归调用
 *      老的尾节点和新的尾节点一样，递归调用
 *      如果老的开始节点和新的尾节点一样，把老的开始节点放到尾部，然后再进行比较
 *      如果老的尾节点和新的开始节点一样，把老的尾节点放到最前边，然后再进行比较
 *      如果不存在上述情况，找到新节点开始节点，挨个去比较，如果和老的某个节点相等了，把老的这个节点放前边去，然后再比较
 *      当新的开始索引大于新的结束索引并且老的开始索引大于老的结束索引，跳出循环，说明比较完了
 *      如果老的少新的多，往老的上边加，新的少，老的多，则删除
 *  如果存在文本节点，看新老文本节点是否一样，不一样直接替换掉

 * 
 * 
 * 如果新节点和老节点一样，根据子节点情况接着比较
 * 老节点有子节点新节点没有，删除老节点子节点
 * 老节点没有子节点新节点有子节点，老节点新增子节点
 * 都只有文本节点，直接替换
 * 都有子节点，继续判断新节点的文本节点是否为undfiend，若是，比较新旧节点的子节点是否都存在，都存在但是不一样，调用updateChildrem方法更新欧美
 * 新节点有子节点老节点没有，将新节点的子节添加到elm中
 * 新节点没有子节点老节点有，移除老节点中的子节点
 * 老节点的文本节点存在的话，把老节点对应元素elm的文本设置为空
 * 若新节点的文本不是空，把元素elm中的文本设置成新的内容
 * 
 * comoputed：当这个函数第一次执行的时候，函数中调用vue中的某个属性的时候，会触发对应属性依赖收集，把这个函数放到对应的依赖收集中，
 * 以后每当这个属性发生变化的时候，就会触发依赖收集中的函数
 * 
 * filter中不能用this
 * computed 适用于一对多的场景，watch适用于多对一的场景
 * watch监听对象时，只能监听整个对象，属性改变并不能触发watch，watch的时候可以加上deep这个属性
 * Vue.use(对象或者一个函数) 若是一个对象，他会默认去找里边的install这个方法，若是方法，则直接当作函数执行
 * const requireComponent = require.context('./', false, /\.vue$/),
 * 读取当前目录下所有.vue的文件，false不遍历子文件夹，true遍历所有子文件夹
 * requireComponent.keys()能获取到所有文件的名字，值为数组
 * requireComponent('./button.vue') 返回文件内容
 * dom的更新是一个异步操作，为了节省性能，不会频繁的渲染页面
 * [1,2].slice(0) 复制一个新数组
 * 
 * $nextTick原理：
 * this.$nextTick(箭头函数)
 * nextTick做了一个异步操作，利用了事件池，把用户传递给nextTick的回调函数（实际上源码把回调函数的基础上又套了一层函数）放到事件池中，
 * 让timerFunc执行，里边调用了异步（优先使用微任务，没有的话使用宏任务），在异步函数中，执行了flushCallbacks，
 * flushCallbacks就是让任务池中的函数执行
 * 
 * vuex专门用来解决组件之间的交互
 * mutation里边的函数必须都是同步的，写异步的话可以执行不报错，不会造成数据的丢失和其他影响，能正常执行。但是通过调试工具去查看，异步mutation
 * 和预期不一样，会造成状态改变的不可追踪。
 * 
 * hash利用的是onhashchange事件，history利用的是onpopstate和pushstate replaceState
 * $route和$router的区别
 * push和replace的区别
 */


/**
 * 手写mvvm
 * 核心：通过Object.defineProperty劫持data属性的setter和getter。在数据变动时发布消息给订阅者，触发相应的监听回调。
 * 创建一个监听器Observe，对数据对象进行遍历，通过Object.defineProperty进行拦截，给每个属性添加set和get，这样当数据进行更新的时候就会
 * 触发set函数，就能监听到数据的改变。
 * 创建一个解析器。compile，解析模板指令，将模板中的变量替换成数据，初始化页面视图，每个指令对应的节点绑定更新函数，添加监听数据的订阅者，
 * 一旦数据有变动，收到通知，更新视图。
 * 创建一个watcher订阅者，等到属性变动收到通知时，触发解析器中绑定的回调函数进行数据更新。
 */
// 劫持data
function observe(data) {
    // 判断data是不是一个纯对象
    if(({}.toString.call(data)) !== '[object Object]') return;
    let keys = Object.keys(data); // 获取所有属性，为每一个对象进行劫持
    keys.forEach(key => {
        defienReactive$$1(data, key, data[key])
    })
}
// 劫持对象属性
function defienReactive$$1 (target, key, val) {
    var dep = new Dep(); // 每一个key都有自己的dep
    Object.defineProperty(target, key , {
        enumerable: true, // 可枚举
        get() {
            // 能够触发get的方式有很多种
            if(Dep.target) {
                dep.add(Dep.target) // 我们在事件池中放的都是订阅者
            }
            return val
        },
        set(newV) {
            if(newV !== val) {
                val = newV;
                dep.notify();
            }
        }
    })
}
function nodeToFragment (el, vm) {
    // 把文档上的节点转到了文档碎片上
    let fragment = document.createDocumentFragment();
    let child;
    while(child = el.firstChild) {
        // child是每一个节点，我们要把每一个节点都编译了
        compile(child, vm);
        fragment.appendChild(child)
    }
    el.appendChild(fragment)
}
function compile(node, vm) {
    // 编译node节点：先判断节点类型，看他是元素节点还是文本节点，元素节点我们要考虑的时候行内属性和他的子节点
    // 文本节点直接进行替换即可
    // 1 3 8 9
    if(node.nodeType == 1) {
        // 这是一个元素节点
        let attrs = node.attributes; // 获取所有的行内属性
        [...attrs].forEach(item => {
            // 我们需要的是 v-xxx的行内属性 v-model mode = 'name'
            if(/^v-/.test(item.nodeName)) {
                // 证明我们获取的是v-model，我们需要v-model后边的值
                let vName = item.nodeValue; // vName就是v-model后边的值
                let val = vm.$data[vName];
                new Watcher(node, vName, vm);
                node.value = val;
                node.addEventListner('input', e => {
                    vm.$data[vName] = e.target.value
                })
            }
        });
        // 以上处理的是行内属性 还有子节点等待去处理
        [...node.childNodes].forEach(item => {
            // 递归遍历子节点
            compile(item, vm)
        })
    } else {
        // 文本节点 我们需要获取
        let str = node.textContent;
        if(/\{\{\w+()\}\}/.test(str)) {
            str = str.replace(/\{\{\w+()\}\}/, (a, b) => {
                return vm.$data[b]
            })
        }
        node.textContent = str
    }
}

/**
 * 观察者模式
 */
class Dep {
    // 每一个属性都应该有自己的订阅器
    constructor() {
        this.subs = []
    }
    add(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.forEach(sub => {
            // 负责通知各个事件执行
            this.sub.update()
        })
    }
}
/**
 * 创建一个订阅者
 */
class Watcher {
    constructor(node, key ,vm) {
        Dep.target = this; // this就是watcher实例
        this.node = node;
        this.key = key;
        this.vm = vm;
        this.getValue(); // 可以把当前的这个watcher实例放到对应的事件池中
        Dep.target = null;
    }
    getValue() {
        this.value = this.vm.$data[this.key] // 会触发这个属性的get
    }
    update() {
        this.getValue(); // 获取新的value值
        // 负责更新dom
        if(this.node.nodeType === 1) {
            this.node.value = this.value
        } else {
            this.node.textContent = this.value
        }
    }
}

function Vue(option) {
    /**
     * 私有属性 $el 对应的是我们的App元素
     * 私有属性 $data对应的是我们传进来的data
     */
    this.$el = document.getElementById(option.el)
    this.$data = option.$data
    
    // 先去数据劫持
    observe(this.$data)
    // 把模板中的vue语法转成真实变量
    nodeToFragment(this.$el, this)
}


/**
 * 图片懒加载，我们在dom渲染完毕后再去请求图片的链接，我们可以把图片的链接地址作为一个自定义属性存在临时盒子中，window.onload或者使用setTimeout
 * 定时器的方法中去调用我们提前封装好的让图片加载的函数。在重新给img的src赋值以后，再调用一下img.onload。这样在图片请求回来以后，我们把原来的
 * loading图去掉。我们也可以在图片加载完成后给图片盒子添加一个属性isload true，在循环图片列表的时候，我们先获取一下是否处理过，如果处理过，就不
 * 再处理了。
 * 在页面滚动的时候，把出现在视口中的图片加载，没有出现的不进行加载。offsetTop获取元素距离父元素的距离。offsetParent获取元素的最近的一个具有
 * 定位的祖宗元素。这样我们使用一个while循环，就能拿到当前元素距离html页面顶部的距离。offsetHeight获取自身的高度。clientHeight网页可见区域的
 * 高度，和document.documentElement.scrollTop的高度。他们的差值进行比较。
 * 基于getBoundingClientRect，获取盒子四个边距离当前视口左上角的位置。这个方法兼容性也很好。
 * 
 * 
 * vue.use()先去看传进来这个值是不是存在intall并且是不是个函数，如果是直接执行，如果不是看看传进来的是不是个函数，
 * 如果是函数，就直接执行。
 * 
 * 默认情况下，模块内部的actions.mutation和getter是注册在全局命名空间下的，加上命名空间以后，模块被注册后他所有的getter、actions、mutations
 * 都会自动根据模块注册的路径调整命名。如果没加命名空间，则会把所有模块中名字是xxx的都执行了。
 * 
 * axios的二次配置
 * axios的优先级，我们在调用请求的时候传进来的配置项的优先级最高，其次是查找default配置，如果都没有，使用axios的默认配置项。
 * 1、baseURL请求地址
 * 项目开发的时候区分是开发环境还是测试环境还是线上环境process.env._NODE_ENV
 * axiso.default.baseURL = ''
 * axios.default.heders.common['Content-Type'] = 'application-x-www-form-urlencode'
 * 2、transformRequest
 * 把请求主体的数据格式转换成我们想要的
 * 如果不处理，transformRequest
 * 我们传递的是一个纯对象，默认处理成json格式
 * 传递的是一个arraybuffer，默认处理成arraybuffer格式传递给服务器
 * 传递的是一个formdata格式数据，则默认按照formdata格式进行处理
 * 传递的的base64格式，则默认按照base64进行处理
 * 如果设置了transformrequest
 * 在发给服务器之前必须先经过transformrequst处理，这样导致formdata等特殊格式，无法和纯对象处理方案一致，此时需要特殊处理
 * 常规处理方案可以基于请求头信息进行判断。要求必须设置对应格式的请求头
 * 调用get、post请求的时候，传一个配置参数，设置请求头，然后再transformrequest中就能拦截，根据不同请求头进行数据格式处理
 * qs库帮我们把对象变成urlencode格式或者把urlencode格式变成对象，Qs.stringfify
 * 
 * axios.defaults.transformRequest = function(data, header) {
 *  let contenttype = header.common['Content-Type']
 * if() {
 *  
 * }
 *  return data
 * }
 * 
 * 请求超时处理，请求时间太长按错误处理
 * axios.defaults.timeout = 10000;
 * 在跨域的时候是否允许携带请求凭证
 * axios.defaults.withCredentials = true
 * axios.default.validateStatus = function(status) {}
 * 
 * 响应拦截器
 * axios.interceptors.response.use(function(res){
 *  return res.data 
 * }, function(reason){
 *  失败、网络状态码
 * let response = reason.resppnse
 * // 如果response存在，一定是axios失败，状态码返回的不是200
 * // response不存在，就是断网，超时，中断请求等
 * if(response) {
 *  
 * } else{
 *    if(resson.code ==='超时英文') {alert('断网')}
 *    if(!navigator.onLine){alert('断网')}
 * }
 * })
 * 
 * // myRouter，源码3000多行

 */
function install(_vue) {
    Vue = _vue;
    Vue.mixin({
        beforeCreate() {
            if(this.$options.router) {
                this.$router = this.$options.rouetr
            } else if(this.$parent) {
                this.$router = this.$parent.router
            }
        }
    })
    Vue.component('router-link', Link);
    Vue.component('router-view', {
        render(h) {
            // this指向当前组件，当前组件能拿到this.$router,this.$router.routerMap就是路由和路径映射表
            // this._route指的是当前路径
            return h(this.$router.routerMap[_this.$router._route])
        }
    })
}
class VueRouter {
    constructor(options) {
        let { routes } = options;
        this.routerMap = {} // 组件和路由的对应关系
        routes.forEach(item=>{
            this.routerMap[item.path] = item.component
        })
        // 把_route做成响应式的, this指的是router
        Vue.util.defineReactive(this, '_route', '/')
        //当前路径
        this._route = location.hash.slice(1);
        // 监听路由改变
        window.onhashchange = () => {
            this._route = location.hash.slice(1)
        }
    }
}
VueRouter.install = install

/**
 * vuex
 */
class vuex {
    constructor(options) {
        let vm = new Vue({
            data() {
                return {
                    state: options.state
                }
            }
        });
        this.state = vm.state
        this.mutations = {}
        let mutations = options.mutations;
        Object.keys(mutations).forEach((key)=>{
            this.mutations[key] = (option) => {
                mutations[key].call(this, this.state, option)
            }
        })
        this.getters = {}
        let getters = options.getters || {}
        Object.keys(getters).forEach(key=>{
            Object.defineProperty(this.getters, key, {
                get: ()=>{
                    return getters[key].call(this, this.state)
                }
            })
        })
    }
    commit(mutationsFnName, option) {
        this.mutations[mutationsFnName](option)
    }
}
export function mapState(ary) {
    let obj = {}
    ary.forEach(key=>{
        obj[key] = function(){
            return this.$store.state[key]
        }
    })
    return obj
}

/**
 * vue2.0 options写法，vue3.0叫composition写法，组合式的写法，setup函数是composition API的入口，在setup函数中定义的变量和方法最后都是
 * 需要return出去的，不然在模板中是无法使用的
 * ref()实现基本类型数据的响应，通过name.value获取name的值
 * reactive()实现引用类型的响应
 * 最后在setup函数里边return出去
 * 
 */
export default {
    setup(props, ctx) {
        /**
         * computed写法
         */
        let type = computed(()=>{
            return count.value % 2 ? '奇数':'偶数'
        });
        let type2 = computed({
            get: ()=>count.value%2?'奇数':'偶数',
            set: (val) => {return val}
        });
        /**
         * watch写法
         * 1、普通数据
         * 2、对象某个属性
         * 3、多个数据
         */
        watch(count, (newVal, oldVal)=>{
            console.log(newVal, oldVal)
        });
        watch(()=>{
            ()=>Object.name,
            (newV, oldV)=>{
                console.log(newV, oldV)
            }
        });
        watch([count, type], (newV, oldV)=>{
            console.log(newV, oldV)
        })
        return {
            type
        }
    },
    mounted() {

    }
}

// 组件库开发流程
/**
 * 模仿element ui
 * package文件夹下，存放各种.vue组件
 * 然后index.js中import引入这些vue组件
 * const requireComponent = require.context('./',false,/\.vue$/);
 * requireComponent.keys()方法获取获取文件路径+名字，数组格式 => ['./button.vue', './input.vue']
 * requireComponent(文件路径)，返回文件内容等信息，对象格式 => requireComponent('./button.vue');
 * function install(_vue) {
 *  requireComponent.keys().forEach(item=>{
 *      const config = requireComponent(item)
 *      _vue.component(config.default.name, config.default)
 *  })
 *  // 全局组件
 *  // _vue.component('my-button', button)
 * }
 * export default install
 */